CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
sagemathinc

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/next/pages/news/[id]/[timestamp].tsx
Views: 687
1
/*
2
* This file is part of CoCalc: Copyright © 2023 Sagemath, Inc.
3
* License: MS-RSL – see LICENSE.md for details
4
*/
5
6
import { Alert, Breadcrumb, Col, Layout, Radio, Row } from "antd";
7
8
import { GetServerSidePropsContext } from "next";
9
import { useRouter } from "next/router";
10
import TimeAgo from "timeago-react";
11
12
import { getNewsItemUser } from "@cocalc/database/postgres/news";
13
import { Icon } from "@cocalc/frontend/components/icon";
14
import { slugURL } from "@cocalc/util/news";
15
import Footer from "components/landing/footer";
16
import Head from "components/landing/head";
17
import Header from "components/landing/header";
18
import A from "components/misc/A";
19
import { News } from "components/news/news";
20
import { NewsWithFuture } from "components/news/types";
21
import { useDateStr } from "components/news/useDateStr";
22
import { MAX_WIDTH, NOT_FOUND } from "lib/config";
23
import { Customize, CustomizeType } from "lib/customize";
24
import useProfile from "lib/hooks/profile";
25
import { extractID } from "lib/news";
26
import withCustomize from "lib/with-customize";
27
28
interface Props {
29
customize: CustomizeType;
30
news: NewsWithFuture;
31
timestamp: number; // unix epoch in seconds
32
prev?: number;
33
next?: number;
34
}
35
36
export default function NewsPage(props: Props) {
37
const { customize, news, timestamp, prev, next } = props;
38
const { siteName } = customize;
39
const router = useRouter();
40
const profile = useProfile({ noCache: true });
41
const isAdmin = profile?.is_admin;
42
const permalink = slugURL(news);
43
const dateStr = useDateStr(news, true);
44
45
const { id } = news;
46
const title = `${news.title}@${dateStr} – News – ${siteName}`;
47
48
function future() {
49
if (news.future && !isAdmin) {
50
return (
51
<Alert type="info" banner={true} message="News not yet published" />
52
);
53
}
54
}
55
56
function content() {
57
if (isAdmin || !news.future) {
58
return <News news={news} showEdit={isAdmin} historyMode standalone />;
59
}
60
}
61
62
function breadcrumb() {
63
const items = [
64
{ key: "/", title: <A href="/">{siteName}</A> },
65
{ key: "/news", title: <A href="/news">News</A> },
66
{ key: "permalink", title: <A href={permalink}>#{news.id}</A> },
67
{
68
key: "timestamp",
69
title: (
70
<A href={`/news/${news.id}/${timestamp}`}>
71
<TimeAgo datetime={1000 * timestamp} />
72
</A>
73
),
74
},
75
];
76
return <Breadcrumb items={items} />;
77
}
78
79
function up() {
80
return (
81
<Radio.Group buttonStyle="outline" size="small">
82
<Radio.Button
83
disabled={!prev}
84
style={{ userSelect: "none" }}
85
onClick={() => {
86
prev && router.push(`/news/${id}/${prev}`);
87
}}
88
>
89
<Icon name="arrow-left" /> Older
90
</Radio.Button>
91
<Radio.Button
92
style={{ userSelect: "none" }}
93
onClick={() => {
94
router.push(slugURL(news));
95
}}
96
>
97
<Icon name="arrow-up" /> Current
98
</Radio.Button>
99
<Radio.Button
100
disabled={!next}
101
style={{ userSelect: "none" }}
102
onClick={() => {
103
next && router.push(`/news/${id}/${next}`);
104
}}
105
>
106
<Icon name="arrow-right" /> Newer
107
</Radio.Button>
108
</Radio.Group>
109
);
110
}
111
112
function renderTop() {
113
return (
114
<Row justify="space-between" gutter={15} style={{ margin: "30px 0" }}>
115
<Col>{breadcrumb()}</Col>
116
<Col>{up()}</Col>
117
</Row>
118
);
119
}
120
121
return (
122
<Customize value={customize}>
123
<Head title={title} />
124
<Layout>
125
<Header />
126
<Layout.Content
127
style={{
128
backgroundColor: "white",
129
}}
130
>
131
<div
132
style={{
133
minHeight: "75vh",
134
maxWidth: MAX_WIDTH,
135
padding: "30px 15px",
136
margin: "0 auto",
137
}}
138
>
139
{renderTop()}
140
{future()}
141
{content()}
142
</div>
143
<Footer />
144
</Layout.Content>
145
</Layout>
146
</Customize>
147
);
148
}
149
150
export async function getServerSideProps(context: GetServerSidePropsContext) {
151
const { query } = context;
152
153
const id = extractID(query.id);
154
if (id == null) return NOT_FOUND;
155
156
// we just re-use the logic for the id
157
const timestamp = extractID(query.timestamp);
158
if (timestamp == null) return NOT_FOUND;
159
160
try {
161
const news = await getNewsItemUser(id);
162
if (news == null) {
163
throw new Error(`not found`);
164
}
165
166
const { history } = news;
167
168
if (history == null) return NOT_FOUND;
169
170
const historic = history[timestamp];
171
if (historic == null) {
172
throw new Error(`history ${timestamp} not found`);
173
}
174
175
// sort keys in news.history by their timestamp value
176
const timestamps = Object.keys(history)
177
.map((ts) => Number(ts))
178
.filter((ts) => !Number.isNaN(ts))
179
.sort((a, b) => a - b);
180
// prev and next are the timestamps of the previous and next news item
181
const prev = timestamps[timestamps.indexOf(timestamp) - 1] ?? null;
182
const next = timestamps[timestamps.indexOf(timestamp) + 1] ?? null;
183
184
return await withCustomize({
185
context,
186
props: {
187
timestamp,
188
prev,
189
next,
190
news: { ...news, ...historic, date: timestamp },
191
},
192
});
193
} catch (err) {
194
console.warn(`Error getting news with id=${id}`, err);
195
}
196
197
return NOT_FOUND;
198
}
199
200